跳到主要内容

MySQL 中的乐观锁和悲观锁

悲观锁(Pessimistic Locking)和乐观锁(Optimistic Locking)是并发控制中两种常见的锁策略。

悲观锁

悲观锁:悲观锁假设在整个事务过程中会发生并发冲突,因此默认情况下会对数据加锁,以防止其他事务修改数据。当一个事务获取了悲观锁后,其他事务无法对该数据进行修改,只能等待锁的释放。

在 MySQL 中,可以使用以下方式实现悲观锁:

  • 使用 SELECT ... FOR UPDATE 语句:通过在查询中添加 FOR UPDATE 子句,MySQL 会对查询结果加上排他锁,阻塞其他事务对该数据的修改。
  • 使用 LOCK TABLES 语句:通过锁定整个表,可以阻止其他事务对表中任何数据的修改,直到锁被释放。

一个实际使用悲观锁的例子是在电子商务系统中对商品库存进行管理。考虑以下情况:

假设有多个用户同时试图购买同一件商品,而商品的库存数量有限。为了避免超卖(卖出超过库存数量)的情况发生,可以使用悲观锁来保证数据的一致性。

在这个例子中,可以使用悲观锁来实现以下逻辑:

  1. 当用户发起购买请求时,首先查询商品的库存数量,并对该行记录进行悲观锁定。
  2. 如果库存数量大于 0,表示商品可售,减少库存数量并生成订单。
  3. 如果库存数量等于 0,表示商品已售完,拒绝购买请求。

使用悲观锁可以保证同时进行的购买请求只有一个能够成功执行,其他请求会被阻塞,直到锁被释放。这样可以避免出现超卖的情况,保证库存的正确性。

在 MySQL 中,可以使用 SELECT ... FOR UPDATE 语句来实现悲观锁。例如:

START TRANSACTION;

SELECT stock FROM products WHERE id = 'product_id' FOR UPDATE;

-- 检查库存数量并执行相应操作

COMMIT;

这个例子中,通过对库存记录加悲观锁,保证了在同一时刻只有一个事务能够成功执行购买操作,其他事务会等待锁的释放。

需要注意的是,在使用悲观锁时应尽量减少锁定的范围和时间,避免对整个表或大量数据进行锁定,以提高并发性能。

乐观锁

乐观锁:乐观锁假设事务之间很少发生冲突,因此不会直接对数据加锁,而是在提交数据时检查是否发生了冲突。如果发现冲突,会回滚事务或重新尝试操作。

在 MySQL 中,可以使用以下方式实现乐观锁:

  • 使用版本号(Versioning):为表中的数据添加一个版本号字段,每次更新数据时增加版本号。在提交数据时,检查数据的版本号是否与事务开始时的版本号相同,如果不同则表示发生了冲突。
  • 使用 CAS(Compare and Set)操作:在更新数据时,使用原子性的 CAS 操作来比较当前值与事务开始时的值,如果相同则进行更新,否则表示发生了冲突。

一个实际使用乐观锁的例子是在博客系统中对博文进行并发编辑的场景。考虑以下情况:

假设有多个用户同时编辑同一篇博文,为了避免数据冲突和丢失更新的问题,可以使用乐观锁来实现数据的一致性。

在这个例子中,可以使用乐观锁来实现以下逻辑:

  1. 当用户开始编辑博文时,首先获取博文的版本号。
  2. 用户在编辑过程中,其他用户也可以尝试编辑相同的博文。
  3. 当用户完成编辑并提交更新时,比较当前博文的版本号与开始编辑时获取的版本号是否一致。
  4. 如果版本号一致,表示期间没有其他用户修改过博文,可以执行更新操作。
  5. 如果版本号不一致,表示博文已被其他用户修改,需要通知用户重新编辑或合并修改。

使用乐观锁可以在不加锁的情况下允许多个用户同时编辑博文,避免了悲观锁的阻塞和等待,提高了并发性能。

在数据库中,可以通过在表中添加一个版本号字段(例如 version)来实现乐观锁。每次更新博文时,需要同时更新版本号字段。例如:

-- 用户开始编辑博文,获取版本号
SELECT version FROM posts WHERE id = 'post_id';

-- 用户编辑完成,提交更新
UPDATE posts SET title = 'New Title', content = 'New Content', version = version + 1 WHERE id = 'post_id' AND version = 'current_version';

-- 检查更新影响的行数,判断是否更新成功

这个例子中,通过比较版本号来判断是否发生了冲突,只有在版本号匹配的情况下才会执行更新操作,否则需要处理冲突。

需要注意的是,乐观锁需要开发人员自行处理冲突和回滚操作,因此在实现乐观锁时需要谨慎处理并发更新的场景,确保数据的一致性。

使用事务与使用乐观锁

为什么不直接使用事务,而是使用乐观锁呢? 它们有什么区别呢?

使用事务和使用乐观锁是针对不同的并发控制需求和场景选择的策略。

事务是数据库提供的一种机制,用于将多个操作作为一个逻辑单元执行,并提供了原子性、一致性、隔离性和持久性的特性。事务适用于需要保证多个操作的一致性和隔离性的场景,例如数据的插入、更新和删除操作。事务的实现通常会涉及锁机制,当并发性要求较高时,可能会导致锁竞争和性能瓶颈。

乐观锁则是一种轻量级的并发控制策略,它假设并发冲突较少发生,不直接对数据加锁,而是在提交更新时检查数据是否发生冲突。乐观锁的实现通常依赖于数据版本号或者CAS(Compare and Set)操作。乐观锁避免了对数据的长时间锁定,可以提高并发性能,特别适用于读多写少的场景。然而,使用乐观锁需要开发人员自行处理冲突的情况,通常需要重新尝试操作或者回滚事务。

选择使用乐观锁的原因可能包括以下几点:

  1. 并发冲突较少:在特定的业务场景中,并发冲突的概率较低,大部分情况下更新操作都不会发生冲突。
  2. 高并发性能要求:乐观锁避免了长时间的锁定,允许多个事务并发执行,提高了系统的并发性能。
  3. 冲突处理较为简单:开发人员可以根据业务逻辑,通过版本号比较或 CAS 操作来检测和处理冲突,从而保证数据的一致性。

总之,使用乐观锁的场景通常是在并发冲突较少且性能要求较高的情况下,开发人员愿意付出处理冲突的额外工作来提高并发性能。而事务则更适用于需要保证多个操作的一致性和隔离性的场景,尤其是对于更新操作较为频繁的情况。

三种加锁方式的区别

事务的方式

事务:事务是一种将多个数据库操作作为一个逻辑单元执行的机制,要么全部成功执行,要么全部回滚。事务通过隔离级别、锁机制和日志恢复等机制来保证并发操作的一致性和隔离性。

区别:

  • 事务是在数据库层面上提供的一种并发控制机制,涉及到多个数据库操作,可以跨多个表和行。
  • 事务提供了原子性、一致性、隔离性和持久性的特性,通过锁定机制来控制并发访问。
  • 事务通常适用于跨越多个操作的复杂业务场景,可以在一次操作中保证数据的完整性和一致性。

乐观锁的方式

乐观锁:乐观锁假设并发冲突很少发生,不直接对数据加锁,而是在提交更新时检查数据是否发生冲突。乐观锁依赖于数据版本号或CAS操作来判断是否发生冲突,如果发生冲突,则需要处理冲突并重试操作。

区别:

  • 乐观锁是一种轻量级的并发控制策略,不需要显式地加锁和释放锁。
  • 乐观锁假设并发冲突较少,适用于读多写少的场景,能够提高并发性能。
  • 乐观锁需要开发人员自行实现冲突检测和处理逻辑,处理冲突的方式可以是回滚操作或重新尝试。

悲观锁的方式

悲观锁:悲观锁假设在整个事务过程中会发生并发冲突,因此默认情况下会对数据加锁,以防止其他事务修改数据。悲观锁通过锁定机制来阻塞其他事务对数据的访问,只有获取锁的事务可以执行操作。

区别:

  • 悲观锁是一种重量级的并发控制策略,需要显式地加锁和释放锁。
  • 悲观锁假设并发冲突较多,适用于写多的场景,保证数据的一致性和隔离性。
  • 悲观锁由数据库管理系统自动处理,并发访问会被阻塞,直到锁被释放。

总结

需要注意的是,乐观锁需要开发人员自行实现冲突检测和处理逻辑,而悲观锁由数据库管理系统自动处理。选择使用悲观锁还是乐观锁取决于并发访问的特点和数据的更新频率。

在实际应用中,可以根据具体的需求和场景选择适合的锁策略,并结合事务、并发控制和业务逻辑来保证数据的一致性和并发性。